package pl.mmadej.ds4java.ds.hash;

import pl.mmadej.ds4java.ds.array.ArrayDecorator;
import pl.mmadej.ds4java.ds.list.LinkedList;

public class ArrayHashMap<K, V> implements HashMap<K, V> {

    private static final int MAGIC_PRIME_NUMBER = 0x27d4eb2d;

    private ArrayDecorator<LinkedList<MapEntry>> array;

    private int size = 0;
    private double loadFactor;

    public ArrayHashMap(int size, double loadFactor) {
        assertLoadFactor(loadFactor);
        this.loadFactor = loadFactor;
        initializeArray(size);
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public V search(K key) {
        for (MapEntry mapEntry : array.elementAt(indexForKey(key))) {
            if (mapEntry.key.equals(key)) {
                return mapEntry.value;
            }
        }
        return null;
    }

    @Override
    public boolean delete(K key) {
        boolean wasRemoved = array.elementAt(indexForKey(key)).delete(new MapEntry(key, null));
        size = wasRemoved ? size - 1 : size;
        return wasRemoved;
    }

    @Override
    public void insert(K key, V value) {
        if (shouldResize()) {
            doubleArray();
        }
        delete(key);
        append(key, value);
    }

    @Override
    public boolean empty() {
        return size() == 0;
    }

    private void assertLoadFactor(double loadFactor) {
        if (loadFactor <= 0 || loadFactor > 1) {
            throw new IllegalArgumentException();
        }
    }

    private void initializeArray(int size) {
        array = new ArrayDecorator<LinkedList<MapEntry>>(size);
        fillArrayWitmEmptyLists();
    }

    private void fillArrayWitmEmptyLists() {
        for (int index = 0; index < array.length(); index++) {
            array.setAt(index, new LinkedList<MapEntry>());
        }
    }

    private void append(K key, V value) {
        size++;
        int index = indexForKey(key);
        array.elementAt(index).insert(new MapEntry(key, value));
    }

    private int indexForKey(K key) {
        return Math.abs(hash(key.hashCode()) % array.length());
    }

    private boolean shouldResize() {
        return (size + 1) / (double) array.length() > loadFactor;
    }

    private void doubleArray() {
        ArrayDecorator<LinkedList<MapEntry>> oldArray = array;
        initializeArray(array.length() * 2);
        size = 0;
        for (int index = 0; index < oldArray.length(); index++) {
            for (MapEntry mapEntry : oldArray.elementAt(index)) {
                insert(mapEntry.key, mapEntry.value);
            }
        }
    }

    private int hash(int key) {
        key = (key ^ 61) ^ (key >>> 16);
        key = key + (key << 3);
        key = key ^ (key >>> 4);
        key = key * MAGIC_PRIME_NUMBER;
        key = key ^ (key >>> 15);
        return key;
    }

    //only for internal use
    private class MapEntry {
        private V value;
        private K key;

        private MapEntry(K key, V value) {
            this.value = value;
            this.key = key;
        }

        @Override
        @SuppressWarnings("unchecked")
        public boolean equals(Object obj) {
            //no need to do instance of, internal use
            return key.equals(((MapEntry) obj).key);
        }
    }
}
